#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
文件同步模块 - 负责目录同步和文件备份功能

这个模块提供了文件同步功能，包括：
1. 单向同步（将源目录文件复制到目标目录）
2. 双向同步（两个目录之间相互同步文件）
3. 增量备份（只复制新的或已修改的文件）
4. 自动冲突检测和处理

使用示例：

1. 单向同步（将源目录的文件同步到目标目录）：
   ```python
   # 预览同步结果，显示将要执行的操作但不实际执行
   changes = sync.sync_directories("/path/source", "/path/target", mode="one-way", preview=True)
   for action in changes:
       print(f"{action['action']}: {action['path']}")
       
   # 执行同步
   sync.sync_directories("/path/source", "/path/target", mode="one-way")
   ```

2. 双向同步（两个目录互相同步）：
   ```python
   # 双向同步两个目录
   sync.sync_directories("/path/dir1", "/path/dir2", mode="two-way")
   ```

3. 增量备份（只同步新文件或修改过的文件）：
   ```python
   # 增量备份，使用时间戳比较策略
   sync.backup("/path/source", "/path/backup", strategy="timestamp")
   ```

4. 冲突处理：
   ```python
   # 同步时指定冲突处理策略
   sync.sync_directories("/path/dir1", "/path/dir2", mode="two-way", 
                         conflict_strategy="newer")
   ```
"""
import os
import shutil
import filecmp
import hashlib
import time
import logging
from pathlib import Path
from typing import List, Dict, Any, Union, Optional, Tuple, Callable
from datetime import datetime

# 设置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def _calculate_file_hash(file_path: Union[str, Path], algorithm: str = "md5", chunk_size: int = 8192) -> str:
    """
    计算文件的哈希值
    
    Args:
        file_path: 文件路径
        algorithm: 哈希算法，默认为md5
        chunk_size: 每次读取的块大小，默认为8KB
        
    Returns:
        str: 文件的哈希值（十六进制字符串）
    """
    path = Path(file_path)
    
    if algorithm == "md5":
        hasher = hashlib.md5()
    elif algorithm == "sha1":
        hasher = hashlib.sha1()
    elif algorithm == "sha256":
        hasher = hashlib.sha256()
    else:
        raise ValueError(f"不支持的哈希算法: {algorithm}")
    
    with path.open("rb") as f:
        chunk = f.read(chunk_size)
        while chunk:
            hasher.update(chunk)
            chunk = f.read(chunk_size)
    
    return hasher.hexdigest()

def _deep_compare_files(file1: Union[str, Path], file2: Union[str, Path]) -> bool:
    """
    深度比较两个文件是否相同
    
    Args:
        file1: 第一个文件路径
        file2: 第二个文件路径
        
    Returns:
        bool: 如果文件内容相同则返回True，否则返回False
    """
    # 先比较文件大小
    path1, path2 = Path(file1), Path(file2)
    if path1.stat().st_size != path2.stat().st_size:
        return False
    
    # 使用filecmp进行快速比较
    if filecmp.cmp(path1, path2, shallow=False):
        return True
    
    # 如果文件较大，使用哈希值比较
    if path1.stat().st_size > 1024*1024:  # 大于1MB的文件
        return _calculate_file_hash(path1) == _calculate_file_hash(path2)
    
    # 对于较小的文件，直接比较内容
    with open(path1, 'rb') as f1, open(path2, 'rb') as f2:
        return f1.read() == f2.read()

def _get_file_info(file_path: Union[str, Path]) -> Dict[str, Any]:
    """
    获取文件的信息
    
    Args:
        file_path: 文件路径
        
    Returns:
        Dict: 包含文件信息的字典
    """
    path = Path(file_path)
    stat = path.stat()
    return {
        'path': str(path),
        'size': stat.st_size,
        'modified': stat.st_mtime,
        'created': stat.st_ctime
    }

def _compare_directories(source_dir: Union[str, Path], target_dir: Union[str, Path], 
                        recursive: bool = True) -> Dict[str, List[str]]:
    """
    比较两个目录，找出需要同步的文件
    
    Args:
        source_dir: 源目录路径
        target_dir: 目标目录路径
        recursive: 是否递归比较子目录
        
    Returns:
        Dict: 包含需要添加、更新和删除的文件列表
    """
    source_path = Path(source_dir)
    target_path = Path(target_dir)
    
    # 确保两个目录都存在
    if not source_path.exists() or not source_path.is_dir():
        raise ValueError(f"源目录不存在或不是目录: {source_dir}")
    
    if not target_path.exists():
        # 如果目标目录不存在，将创建它
        logger.info(f"目标目录不存在，将创建: {target_dir}")
        target_path.mkdir(parents=True, exist_ok=True)
    elif not target_path.is_dir():
        raise ValueError(f"目标路径存在但不是目录: {target_dir}")
    
    # 需要添加或更新的文件
    to_add = []     # 在源中存在但在目标中不存在的文件
    to_update = []  # 在源和目标中都存在但内容不同的文件
    
    # 需要在目标中删除的文件（双向同步时使用）
    to_delete = []  # 在目标中存在但在源中不存在的文件
    
    # 获取源目录中的所有文件
    source_files = []
    if recursive:
        for root, _, files in os.walk(source_path):
            for file in files:
                file_path = Path(root) / file
                rel_path = file_path.relative_to(source_path)
                source_files.append(str(rel_path))
    else:
        source_files = [f.name for f in source_path.iterdir() if f.is_file()]
    
    # 获取目标目录中的所有文件
    target_files = []
    if recursive and target_path.exists():
        for root, _, files in os.walk(target_path):
            for file in files:
                file_path = Path(root) / file
                rel_path = file_path.relative_to(target_path)
                target_files.append(str(rel_path))
    elif target_path.exists():
        target_files = [f.name for f in target_path.iterdir() if f.is_file()]
    
    # 找出需要添加或更新的文件
    for rel_path in source_files:
        source_file = source_path / rel_path
        target_file = target_path / rel_path
        
        # 确保目标文件的父目录存在
        target_file.parent.mkdir(parents=True, exist_ok=True)
        
        if str(rel_path) not in target_files:
            # 目标中不存在此文件，需要添加
            to_add.append(str(rel_path))
        else:
            # 目标中存在此文件，检查是否需要更新
            if not _deep_compare_files(source_file, target_file):
                to_update.append(str(rel_path))
    
    # 找出需要删除的文件（仅在双向同步时使用）
    for rel_path in target_files:
        if str(rel_path) not in source_files:
            to_delete.append(str(rel_path))
    
    return {
        'add': to_add,
        'update': to_update,
        'delete': to_delete
    }

def sync_directories(source_dir: Union[str, Path], target_dir: Union[str, Path], 
                   mode: str = "one-way", recursive: bool = True, 
                   delete: bool = False, preview: bool = False,
                   conflict_strategy: str = "newer") -> List[Dict[str, Any]]:
    """
    同步两个目录之间的文件
    
    Args:
        source_dir: 源目录路径
        target_dir: 目标目录路径
        mode: 同步模式，'one-way'（单向）或'two-way'（双向）
        recursive: 是否递归处理子目录
        delete: 是否删除目标目录中源目录不存在的文件
        preview: 是否只预览而不执行
        conflict_strategy: 冲突解决策略，可选：
            - 'newer': 保留修改时间较新的文件
            - 'older': 保留修改时间较旧的文件
            - 'larger': 保留文件大小较大的文件
            - 'smaller': 保留文件大小较小的文件
            - 'source': 始终以源目录的文件为准
            - 'target': 始终以目标目录的文件为准
        
    Returns:
        List[Dict]: 包含执行的操作列表
    """
    source_path = Path(source_dir)
    target_path = Path(target_dir)
    changes = []
    
    # 比较目录差异
    diff = _compare_directories(source_path, target_path, recursive)
    
    # 单向同步
    if mode == "one-way":
        # 添加新文件
        for rel_path in diff['add']:
            changes.append({'action': 'add', 'path': rel_path})
            if not preview:
                source_file = source_path / rel_path
                target_file = target_path / rel_path
                
                # 确保目标文件的父目录存在
                target_file.parent.mkdir(parents=True, exist_ok=True)
                
                shutil.copy2(source_file, target_file)
                logger.info(f"已添加: {target_file}")
        
        # 更新已有文件
        for rel_path in diff['update']:
            changes.append({'action': 'update', 'path': rel_path})
            if not preview:
                source_file = source_path / rel_path
                target_file = target_path / rel_path
                shutil.copy2(source_file, target_file)
                logger.info(f"已更新: {target_file}")
        
        # 删除目标目录中多余的文件（如果启用删除选项）
        if delete:
            for rel_path in diff['delete']:
                changes.append({'action': 'delete', 'path': rel_path})
                if not preview:
                    target_file = target_path / rel_path
                    target_file.unlink()
                    logger.info(f"已删除: {target_file}")
                    
                    # 删除空目录
                    dir_path = target_file.parent
                    while dir_path != target_path and not any(dir_path.iterdir()):
                        dir_path.rmdir()
                        logger.info(f"已删除空目录: {dir_path}")
                        dir_path = dir_path.parent
    
    # 双向同步
    elif mode == "two-way":
        # 从源到目标的同步
        source_to_target = _compare_directories(source_path, target_path, recursive)
        
        # 从目标到源的同步
        target_to_source = _compare_directories(target_path, source_path, recursive)
        
        # 处理冲突（在两个方向上都需要更新的文件）
        conflicts = set(source_to_target['update']).intersection(target_to_source['update'])
        
        # 根据冲突策略处理冲突文件
        for rel_path in conflicts:
            source_file = source_path / rel_path
            target_file = target_path / rel_path
            
            source_info = _get_file_info(source_file)
            target_info = _get_file_info(target_file)
            
            # 根据冲突策略决定保留哪个文件
            keep_source = False
            
            if conflict_strategy == 'newer':
                keep_source = source_info['modified'] > target_info['modified']
            elif conflict_strategy == 'older':
                keep_source = source_info['modified'] < target_info['modified']
            elif conflict_strategy == 'larger':
                keep_source = source_info['size'] > target_info['size']
            elif conflict_strategy == 'smaller':
                keep_source = source_info['size'] < target_info['size']
            elif conflict_strategy == 'source':
                keep_source = True
            elif conflict_strategy == 'target':
                keep_source = False
            
            if keep_source:
                changes.append({'action': 'resolve_conflict', 'path': rel_path, 'resolution': '使用源文件'})
                if not preview:
                    shutil.copy2(source_file, target_file)
                    logger.info(f"冲突处理 - 使用源文件: {rel_path}")
            else:
                changes.append({'action': 'resolve_conflict', 'path': rel_path, 'resolution': '使用目标文件'})
                if not preview:
                    shutil.copy2(target_file, source_file)
                    logger.info(f"冲突处理 - 使用目标文件: {rel_path}")
        
        # 从源到目标的文件添加和非冲突更新
        for rel_path in source_to_target['add']:
            changes.append({'action': 'add_to_target', 'path': rel_path})
            if not preview:
                source_file = source_path / rel_path
                target_file = target_path / rel_path
                
                # 确保目标文件的父目录存在
                target_file.parent.mkdir(parents=True, exist_ok=True)
                
                shutil.copy2(source_file, target_file)
                logger.info(f"已添加到目标: {target_file}")
        
        for rel_path in source_to_target['update']:
            if rel_path not in conflicts:
                changes.append({'action': 'update_target', 'path': rel_path})
                if not preview:
                    source_file = source_path / rel_path
                    target_file = target_path / rel_path
                    shutil.copy2(source_file, target_file)
                    logger.info(f"已更新目标: {target_file}")
        
        # 从目标到源的文件添加和非冲突更新
        for rel_path in target_to_source['add']:
            changes.append({'action': 'add_to_source', 'path': rel_path})
            if not preview:
                target_file = target_path / rel_path
                source_file = source_path / rel_path
                
                # 确保源文件的父目录存在
                source_file.parent.mkdir(parents=True, exist_ok=True)
                
                shutil.copy2(target_file, source_file)
                logger.info(f"已添加到源: {source_file}")
        
        for rel_path in target_to_source['update']:
            if rel_path not in conflicts:
                changes.append({'action': 'update_source', 'path': rel_path})
                if not preview:
                    target_file = target_path / rel_path
                    source_file = source_path / rel_path
                    shutil.copy2(target_file, source_file)
                    logger.info(f"已更新源: {source_file}")
    
    else:
        raise ValueError(f"不支持的同步模式: {mode}")
    
    return changes

def backup(source_dir: Union[str, Path], target_dir: Union[str, Path], 
         strategy: str = "timestamp", recursive: bool = True, 
         preview: bool = False) -> List[Dict[str, Any]]:
    """
    执行增量备份
    
    Args:
        source_dir: 源目录路径
        target_dir: 目标目录路径
        strategy: 增量备份策略，可选：
            - 'timestamp': 比较文件修改时间
            - 'hash': 比较文件哈希值
            - 'size': 比较文件大小
        recursive: 是否递归处理子目录
        preview: 是否只预览而不执行
        
    Returns:
        List[Dict]: 包含执行的操作列表
    """
    source_path = Path(source_dir)
    target_path = Path(target_dir)
    changes = []
    
    # 确保目录存在
    if not source_path.exists() or not source_path.is_dir():
        raise ValueError(f"源目录不存在或不是目录: {source_dir}")
    
    if not target_path.exists():
        if not preview:
            target_path.mkdir(parents=True, exist_ok=True)
        logger.info(f"已创建备份目录: {target_path}")
    elif not target_path.is_dir():
        raise ValueError(f"目标路径存在但不是目录: {target_dir}")
    
    # 获取源目录中的所有文件
    source_files = []
    if recursive:
        for root, _, files in os.walk(source_path):
            for file in files:
                file_path = Path(root) / file
                rel_path = file_path.relative_to(source_path)
                source_files.append(str(rel_path))
    else:
        source_files = [f.name for f in source_path.iterdir() if f.is_file()]
    
    # 获取目标目录中的所有文件
    target_files = {}
    if recursive and target_path.exists():
        for root, _, files in os.walk(target_path):
            for file in files:
                file_path = Path(root) / file
                rel_path = file_path.relative_to(target_path)
                target_files[str(rel_path)] = file_path
    elif target_path.exists():
        target_files = {f.name: f for f in target_path.iterdir() if f.is_file()}
    
    # 检查每个源文件是否需要备份
    for rel_path in source_files:
        source_file = source_path / rel_path
        target_file = target_path / rel_path
        
        # 确保目标文件的父目录存在
        if not preview:
            target_file.parent.mkdir(parents=True, exist_ok=True)
        
        need_backup = False
        
        # 如果目标文件不存在，需要备份
        if str(rel_path) not in target_files:
            need_backup = True
            reason = "文件不存在于备份中"
        else:
            # 根据策略确定是否需要更新
            if strategy == "timestamp":
                source_mtime = source_file.stat().st_mtime
                target_mtime = target_files[str(rel_path)].stat().st_mtime
                if source_mtime > target_mtime:
                    need_backup = True
                    reason = "源文件更新"
            
            elif strategy == "hash":
                source_hash = _calculate_file_hash(source_file)
                target_hash = _calculate_file_hash(target_files[str(rel_path)])
                if source_hash != target_hash:
                    need_backup = True
                    reason = "文件内容不同"
            
            elif strategy == "size":
                source_size = source_file.stat().st_size
                target_size = target_files[str(rel_path)].stat().st_size
                if source_size != target_size:
                    need_backup = True
                    reason = "文件大小不同"
            
            else:
                raise ValueError(f"不支持的备份策略: {strategy}")
        
        # 执行备份
        if need_backup:
            changes.append({'action': 'backup', 'path': rel_path, 'reason': reason})
            if not preview:
                shutil.copy2(source_file, target_file)
                logger.info(f"已备份: {source_file} -> {target_file} ({reason})")
    
    return changes

def scheduled_sync(source_dir: Union[str, Path], target_dir: Union[str, Path], 
                 interval: int, mode: str = "one-way", 
                 recursive: bool = True, delete: bool = False,
                 conflict_strategy: str = "newer"):
    """
    执行定时同步
    
    Args:
        source_dir: 源目录路径
        target_dir: 目标目录路径
        interval: 同步间隔（秒）
        mode: 同步模式，'one-way'（单向）或'two-way'（双向）
        recursive: 是否递归处理子目录
        delete: 是否删除目标目录中源目录不存在的文件
        conflict_strategy: 冲突解决策略
        
    Note:
        这个函数会一直运行，直到被中断
    """
    source_path = Path(source_dir)
    target_path = Path(target_dir)
    
    logger.info(f"开始定时同步: {source_path} <-> {target_path} (间隔: {interval}秒)")
    
    try:
        while True:
            start_time = time.time()
            
            # 执行同步
            try:
                changes = sync_directories(
                    source_path, target_path, 
                    mode=mode, recursive=recursive, 
                    delete=delete, preview=False,
                    conflict_strategy=conflict_strategy
                )
                
                if changes:
                    logger.info(f"同步完成，处理了 {len(changes)} 个变更")
                else:
                    logger.info("同步完成，没有变更")
            
            except Exception as e:
                logger.error(f"同步出错: {e}")
            
            # 计算下次同步的等待时间
            elapsed = time.time() - start_time
            wait_time = max(0, interval - elapsed)
            
            logger.info(f"下次同步将在 {wait_time:.1f} 秒后进行")
            time.sleep(wait_time)
    
    except KeyboardInterrupt:
        logger.info("定时同步已停止") 